Skip to content

[NOT-861] feat(cli): use session-scoped file API - #70

Open
leo-notte wants to merge 1 commit into
mainfrom
feat/session-scoped-files
Open

[NOT-861] feat(cli): use session-scoped file API#70
leo-notte wants to merge 1 commit into
mainfrom
feat/session-scoped-files

Conversation

@leo-notte

@leo-notte leo-notte commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • require a session for every file operation
  • list uploads and browser downloads through the unified session catalog
  • download files by immutable ID
  • remove the obsolete file-storage session flags

Tests

  • go test ./internal/cmd ./internal/api

Linear: NOT-861 https://linear.app/nottelabsinc/issue/NOT-861/notte-cli-pr-70-featcli-use-session-scoped-file-api

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR moves file upload, listing, and download operations to session-scoped endpoints and removes obsolete file-storage session flags.

  • Requires a session for every file operation and lists unified catalog entries.
  • Downloads files directly by immutable ID and uses response metadata for the default filename.
  • Regenerates session-start API and flag surfaces without the file-storage option.
  • Adds and updates command-level unit tests for the session-scoped flows.

Confidence Score: 3/5

The PR should not merge until the default list operation actually returns all session files and downloads retain appropriate destination permissions.

The default-all list path currently sends a downloads-only filter, while the replacement download path renames 0600 temporary files without restoring new-file or existing-file permissions.

Files Needing Attention: internal/cmd/files.go, internal/cmd/files_test.go

Important Files Changed

Filename Overview
internal/cmd/files.go Implements session-scoped file operations, but the default list request still filters to downloads and downloaded files lose expected permissions.
internal/cmd/files_test.go Updates file-command tests for session endpoints, but does not cover the default-all query or permissions of the actual download command path.
internal/api/client.go Removes the obsolete global uploaded-file download wrapper now that downloads use the session endpoint.
internal/api/client.gen.go Removes obsolete file-storage fields from generated session request and response types.
internal/cmd/sessionstart_flags.gen.go Removes generated registration and request mapping for the obsolete file-storage flag.
internal/cmd/sessionstart_optout.go Removes the corresponding file-storage opt-out while retaining the other session-start opt-outs.

Fix all with Greploop Fix All in Codex

Prompt To Fix All With AI
### Issue 1
internal/cmd/files.go:240-244
**Default listing filters downloads**

When `notte files list` is run without a source flag, the empty default-all value enters this `else` branch and adds `source=session_download`, causing user uploads to be omitted from a command documented to list all session files.

```suggestion
	if source == filesSourceUploads {
		endpoint += "&source=user_upload"
	} else if source == filesSourceSession {
		endpoint += "&source=session_download"
	}
```

### Issue 2
internal/cmd/files.go:390-405
**Download replacement drops permissions**

Every successful download is renamed from an `os.CreateTemp` file without changing its 0600 mode, so new downloads are not ordinarily readable and overwriting an existing readable or executable file silently removes its previous permission bits.

```suggestion
	fileMode := os.FileMode(0o644)
	if info, statErr := os.Stat(destinationPath); statErr == nil {
		if info.IsDir() {
			return fmt.Errorf("destination path is a directory")
		}
		fileMode = info.Mode().Perm()
	} else if !os.IsNotExist(statErr) {
		return fmt.Errorf("failed to inspect destination: %w", statErr)
	}

	temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".notte-download-*")
	if err != nil {
		return err
	}
	temporaryPath := temporary.Name()
	defer func() { _ = os.Remove(temporaryPath) }()
	if _, err := io.Copy(temporary, resp.Body); err != nil {
		_ = temporary.Close()
		return err
	}
	if err := temporary.Chmod(fileMode); err != nil {
		_ = temporary.Close()
		return err
	}
	if err := temporary.Close(); err != nil {
		return err
	}
	if err := os.Rename(temporaryPath, destinationPath); err != nil {
		return err
	}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(cli): use session-scoped file API" | Re-trigger Greptile

Comment thread internal/cmd/files.go
Comment on lines 240 to 244
if source == filesSourceUploads {
ctx, cancel := GetContextWithTimeout(cmd.Context())
defer cancel()

params := &api.FileListUploadsParams{}
resp, err := client.Client().FileListUploadsWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("API request failed: %w", err)
}

if err := HandleAPIResponse(resp.HTTPResponse, resp.Body); err != nil {
return err
}

var fileNames []string
if resp.JSON200 != nil {
for _, f := range resp.JSON200.Files {
fileNames = append(fileNames, f.Name)
}
}
if printed, err := PrintListOrEmpty(fileNames, "No uploaded files."); err != nil {
return err
} else if printed {
return nil
}

if !IsJSONOutput() {
fmt.Println("Your uploaded files:")
}
return formatter.Print(fileNames)
endpoint += "&source=user_upload"
} else {
endpoint += "&source=session_download"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Default listing filters downloads

When notte files list is run without a source flag, the empty default-all value enters this else branch and adds source=session_download, causing user uploads to be omitted from a command documented to list all session files.

Suggested change
if source == filesSourceUploads {
ctx, cancel := GetContextWithTimeout(cmd.Context())
defer cancel()
params := &api.FileListUploadsParams{}
resp, err := client.Client().FileListUploadsWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("API request failed: %w", err)
}
if err := HandleAPIResponse(resp.HTTPResponse, resp.Body); err != nil {
return err
}
var fileNames []string
if resp.JSON200 != nil {
for _, f := range resp.JSON200.Files {
fileNames = append(fileNames, f.Name)
}
}
if printed, err := PrintListOrEmpty(fileNames, "No uploaded files."); err != nil {
return err
} else if printed {
return nil
}
if !IsJSONOutput() {
fmt.Println("Your uploaded files:")
}
return formatter.Print(fileNames)
endpoint += "&source=user_upload"
} else {
endpoint += "&source=session_download"
}
if source == filesSourceUploads {
endpoint += "&source=user_upload"
} else if source == filesSourceSession {
endpoint += "&source=session_download"
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/cmd/files.go
Line: 240-244

Comment:
**Default listing filters downloads**

When `notte files list` is run without a source flag, the empty default-all value enters this `else` branch and adds `source=session_download`, causing user uploads to be omitted from a command documented to list all session files.

```suggestion
	if source == filesSourceUploads {
		endpoint += "&source=user_upload"
	} else if source == filesSourceSession {
		endpoint += "&source=session_download"
	}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Comment thread internal/cmd/files.go
Comment on lines +390 to 405
temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".notte-download-*")
if err != nil {
return err
}

if downloadResp.URL == "" {
return fmt.Errorf("no download URL in response")
temporaryPath := temporary.Name()
defer func() { _ = os.Remove(temporaryPath) }()
if _, err := io.Copy(temporary, resp.Body); err != nil {
_ = temporary.Close()
return err
}

// Determine output path
outputPath := filesDownloadOutput
if outputPath == "" {
outputPath = filename
if err := temporary.Close(); err != nil {
return err
}

if err := downloadFileWithContext(ctx, downloadResp.URL, outputPath); err != nil {
return fmt.Errorf("failed to download file: %w", err)
if err := os.Rename(temporaryPath, destinationPath); err != nil {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Download replacement drops permissions

Every successful download is renamed from an os.CreateTemp file without changing its 0600 mode, so new downloads are not ordinarily readable and overwriting an existing readable or executable file silently removes its previous permission bits.

Suggested change
temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".notte-download-*")
if err != nil {
return err
}
if downloadResp.URL == "" {
return fmt.Errorf("no download URL in response")
temporaryPath := temporary.Name()
defer func() { _ = os.Remove(temporaryPath) }()
if _, err := io.Copy(temporary, resp.Body); err != nil {
_ = temporary.Close()
return err
}
// Determine output path
outputPath := filesDownloadOutput
if outputPath == "" {
outputPath = filename
if err := temporary.Close(); err != nil {
return err
}
if err := downloadFileWithContext(ctx, downloadResp.URL, outputPath); err != nil {
return fmt.Errorf("failed to download file: %w", err)
if err := os.Rename(temporaryPath, destinationPath); err != nil {
return err
}
fileMode := os.FileMode(0o644)
if info, statErr := os.Stat(destinationPath); statErr == nil {
if info.IsDir() {
return fmt.Errorf("destination path is a directory")
}
fileMode = info.Mode().Perm()
} else if !os.IsNotExist(statErr) {
return fmt.Errorf("failed to inspect destination: %w", statErr)
}
temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".notte-download-*")
if err != nil {
return err
}
temporaryPath := temporary.Name()
defer func() { _ = os.Remove(temporaryPath) }()
if _, err := io.Copy(temporary, resp.Body); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Chmod(fileMode); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Rename(temporaryPath, destinationPath); err != nil {
return err
}

Knowledge Base Used: CLI Command Dispatch

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/cmd/files.go
Line: 390-405

Comment:
**Download replacement drops permissions**

Every successful download is renamed from an `os.CreateTemp` file without changing its 0600 mode, so new downloads are not ordinarily readable and overwriting an existing readable or executable file silently removes its previous permission bits.

```suggestion
	fileMode := os.FileMode(0o644)
	if info, statErr := os.Stat(destinationPath); statErr == nil {
		if info.IsDir() {
			return fmt.Errorf("destination path is a directory")
		}
		fileMode = info.Mode().Perm()
	} else if !os.IsNotExist(statErr) {
		return fmt.Errorf("failed to inspect destination: %w", statErr)
	}

	temporary, err := os.CreateTemp(filepath.Dir(destinationPath), ".notte-download-*")
	if err != nil {
		return err
	}
	temporaryPath := temporary.Name()
	defer func() { _ = os.Remove(temporaryPath) }()
	if _, err := io.Copy(temporary, resp.Body); err != nil {
		_ = temporary.Close()
		return err
	}
	if err := temporary.Chmod(fileMode); err != nil {
		_ = temporary.Close()
		return err
	}
	if err := temporary.Close(); err != nil {
		return err
	}
	if err := os.Rename(temporaryPath, destinationPath); err != nil {
		return err
	}
```

**Knowledge Base Used:** [CLI Command Dispatch](https://app.greptile.com/nottelabs/-/custom-context/knowledge-base/nottelabs/notte-cli/-/docs/cli-commands.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

@giordano-lucas giordano-lucas changed the title feat(cli): use session-scoped file API [NOT-861] feat(cli): use session-scoped file API Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant